Skip to content

feat(AssetsController): Measure intial fetch duration#7871

Merged
Kriys94 merged 2 commits intomainfrom
feature/InitMetrics
Feb 13, 2026
Merged

feat(AssetsController): Measure intial fetch duration#7871
Kriys94 merged 2 commits intomainfrom
feature/InitMetrics

Conversation

@Kriys94
Copy link
Contributor

@Kriys94 Kriys94 commented Feb 9, 2026

Explanation

  • Current state / why change: AssetsController runs an initial asset fetch when the user unlocks the wallet or when the app opens. There is no way to report how long that first fetch takes to our analytics. We need to measure this (InitMetrics) so we can track performance in MetaMetrics and spot regressions or improvements in the assets pipeline.

  • Solution / how it works: An optional trackMetaMetricsEvent callback was added to the AssetsController constructor. When the first init/fetch completes (after unlock or app open), the controller invokes this callback once per session with a payload: durationMs (wall-clock duration in ms), chainIds (requested chains, e.g. ['eip155:1', 'eip155:137']), and durationByDataSource (exclusive latency per data source, same order as the middleware chain). A new type AssetsControllerFirstInitFetchMetaMetricsPayload was added and exported so consumers can type the callback. The callback is only fired once per “session”; the internal “reported” flag is reset in #stop() (e.g. on lock), so the next unlock can trigger the event again. Consumers (e.g. the extension) can pass a callback that forwards this payload to MetaMetricsController; the controller does not depend on MetaMetrics directly.

  • Non-obvious changes: durationByDataSource is exclusive latency per data source (sum approximates durationMs). This allows downstream analytics to see which data sources contribute most to init time.

  • Other packages / dependency upgrades: None; changes are confined to @metamask/assets-controller.

References

  • (Add issue numbers or related PRs if applicable; e.g. Fixes #XXXX, or link to extension/mobile PR that wires up trackMetaMetricsEvent.)

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Medium Risk
Touches the core asset fetch pipeline and start/stop lifecycle by refactoring middleware execution and adding new timing/analytics hooks, which could affect initial fetch behavior and ordering if incorrect.

Overview
Adds an optional trackMetaMetricsEvent callback to AssetsController that fires once per unlock/app-open session after the first getAssets({ forceUpdate: true }) completes, reporting total duration, requested chainIds, and a per-source durationByDataSource breakdown.

To support this, middleware execution is refactored to accept sources with getName() and to measure/return exclusive timings per data source/middleware; TokenDataSource, DetectionMiddleware, and PriceDataSource now expose getName() (and PriceDataSource no longer relies on an instance name field). Tests and exports are updated, and the “first init reported” flag resets on #stop() (lock) so the metric can be emitted again next session.

Written by Cursor Bugbot for commit 01f41a9. This will update automatically on new commits. Configure here.

@Kriys94 Kriys94 force-pushed the feature/InitMetrics branch 2 times, most recently from 6ce09f9 to ddf7448 Compare February 13, 2026 08:22
@Kriys94 Kriys94 marked this pull request as ready for review February 13, 2026 08:31
@Kriys94 Kriys94 requested review from a team as code owners February 13, 2026 08:31
hasPriceSubscription: this.#activeSubscriptions.has('ds:PriceDataSource'),
});

this.#firstInitFetchReported = false;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Session metric resets on preference toggle

Medium Severity

#firstInitFetchReported is reset inside #stop(), but #stop() is also called by handleBasicFunctionalityChange(). That allows trackMetaMetricsEvent to fire again without a lock/unlock cycle, so the “first init fetch once per session” guarantee in AssetsController can be violated.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a problem

*/
export type AssetsControllerFirstInitFetchMetaMetricsPayload = {
/** Duration of the first init fetch in milliseconds (wall-clock). */
durationMs: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a duration for all fetch we do ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

* Exclusive latency in ms per data source (time spent in that source only).
* Sum of values approximates durationMs. Order: same as middleware chain.
*/
durationByDataSource: Record<string, number>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and i guess this is duration per data source

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

) => void;

/** Whether we have already reported first init fetch for this session (reset on #stop). */
#firstInitFetchReported = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what this means ? is this reset after each open/close of the app or when the background stop and restart ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

salimtb
salimtb previously approved these changes Feb 13, 2026
Copy link
Contributor

@salimtb salimtb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approving this PR , but it will be great if you can create a preview build and test it on the UI

Comment on lines +103 to +107
readonly name = CONTROLLER_NAME;

getName(): string {
return this.name;
}
Copy link
Contributor

@Prithpal-Sooriya Prithpal-Sooriya Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - thoughts on.

  1. Making the name a static property, we never override this so it can remain static.
  2. We can then remove the method since we can grab the name from the static class.
export class PriceDataSource {
  static readonly controllerName = CONTROLLER_NAME
}

// Elsewhere
const foo = PriceDataSource.controllerName

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or... technically you don't need a name right? Since the class prototype already takes the name of the class?

// The class prototype already has a name of 'PriceDataSource'
export class PriceDataSource {
}

// Elsewhere
const foo = PriceDataSource.name

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I see the implementation below, we want instances to grab the name. Still we can clean this method up by making it point to the static fields (instead of creating a name for every instantiation?)

getName(): string {
  return PriceDataSource.controllerName
}

// or use Class name prototype
getName(): string {
  return PriceDataSource.name
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done thanks :)

@Kriys94
Copy link
Contributor Author

Kriys94 commented Feb 13, 2026

approving this PR , but it will be great if you can create a preview build and test it on the UI

Yeah I've tested it with MetaMask/metamask-extension#40087

salimtb
salimtb previously approved these changes Feb 13, 2026
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

durationMs,
chainIds,
durationByDataSource,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale fetch can misreport session metric

Medium Severity

trackMetaMetricsEvent is emitted based only on #firstInitFetchReported, with no guard that the fetch still belongs to the active session. If #stop() runs while a prior getAssets(..., forceUpdate) is still in flight, that stale fetch can report after stop and set the flag, skewing session attribution and suppressing the next real init metric.

Additional Locations (1)

Fix in Cursor Fix in Web

@Kriys94 Kriys94 added this pull request to the merge queue Feb 13, 2026
Merged via the queue into main with commit 731cd2c Feb 13, 2026
302 checks passed
@Kriys94 Kriys94 deleted the feature/InitMetrics branch February 13, 2026 15:11
khanti42 pushed a commit that referenced this pull request Feb 20, 2026
## Explanation

<!--
Thanks for your contribution! Take a moment to answer these questions so
that reviewers have the information they need to properly understand
your changes:

* What is the current state of things and why does it need to change?
* What is the solution your changes offer and how does it work?
* Are there any changes whose purpose might not obvious to those
unfamiliar with the domain?
* If your primary goal was to update one package but you found you had
to update another one along the way, why did you do so?
* If you had to upgrade a dependency, why did you do so?
-->

* **Current state / why change:** AssetsController runs an initial asset
fetch when the user unlocks the wallet or when the app opens. There is
no way to report how long that first fetch takes to our analytics. We
need to measure this (InitMetrics) so we can track performance in
MetaMetrics and spot regressions or improvements in the assets pipeline.

* **Solution / how it works:** An optional `trackMetaMetricsEvent`
callback was added to the AssetsController constructor. When the
**first** init/fetch completes (after unlock or app open), the
controller invokes this callback once per session with a payload:
`durationMs` (wall-clock duration in ms), `chainIds` (requested chains,
e.g. `['eip155:1', 'eip155:137']`), and `durationByDataSource`
(exclusive latency per data source, same order as the middleware chain).
A new type `AssetsControllerFirstInitFetchMetaMetricsPayload` was added
and exported so consumers can type the callback. The callback is only
fired once per “session”; the internal “reported” flag is reset in
`#stop()` (e.g. on lock), so the next unlock can trigger the event
again. Consumers (e.g. the extension) can pass a callback that forwards
this payload to MetaMetricsController; the controller does not depend on
MetaMetrics directly.

* **Non-obvious changes:** `durationByDataSource` is exclusive latency
per data source (sum approximates `durationMs`). This allows downstream
analytics to see which data sources contribute most to init time.

* **Other packages / dependency upgrades:** None; changes are confined
to `@metamask/assets-controller`.

## References

<!--
Are there any issues that this pull request is tied to?
Are there other links that reviewers should consult to understand these
changes better?
Are there client or consumer pull requests to adopt any breaking
changes?

For example:

* Fixes #12345
* Related to #67890
-->

* (Add issue numbers or related PRs if applicable; e.g. Fixes #XXXX, or
link to extension/mobile PR that wires up `trackMetaMetricsEvent`.)

## Checklist

- [ ] I've updated the test suite for new or updated code as appropriate
- [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [ ] I've communicated my changes to consumers by [updating changelogs
for packages I've
changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md)
- [ ] I've introduced [breaking
changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md)
in this PR and have prepared draft pull requests for clients and
consumer packages to resolve them

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches the core asset fetch pipeline and start/stop lifecycle by
refactoring middleware execution and adding new timing/analytics hooks,
which could affect initial fetch behavior and ordering if incorrect.
> 
> **Overview**
> Adds an optional `trackMetaMetricsEvent` callback to
`AssetsController` that fires **once per unlock/app-open session** after
the first `getAssets({ forceUpdate: true })` completes, reporting total
duration, requested `chainIds`, and a per-source `durationByDataSource`
breakdown.
> 
> To support this, middleware execution is refactored to accept sources
with `getName()` and to measure/return exclusive timings per data
source/middleware; `TokenDataSource`, `DetectionMiddleware`, and
`PriceDataSource` now expose `getName()` (and `PriceDataSource` no
longer relies on an instance `name` field). Tests and exports are
updated, and the “first init reported” flag resets on `#stop()` (lock)
so the metric can be emitted again next session.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
01f41a9. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants